#
#Copyright 2009 VMware, Inc.  All rights reserved. -- VMware Confidential
#

"""
Module for handling the installation side of components in the remote/
older VMIS installer.
"""

from installer import Installer
import inspect
import pickle
import StringIO
import sys
import traceback
import vmispy
import imp

import vmis.vmisdebug as vmisdebug
from vmis.core.errors import *
import vmis.core.files as files
import vmis.util.path as path

# Import possible Errors in the component.
from vmis.core.errors import InstallError

# Duplicated code from vmis/util/__init__.py in order
#  to avoid an import and its resulting chain of imports
import py_compile

def _compilePython(filePath):
   """
   Compile the given Python file

   @returns: path to compiled file
   @raises py_compile.PyCompileError
   """
   compiled = filePath + (__debug__ and 'c' or 'o')
   py_compile.compile(unicode(filePath), compiled)
   return compiled

class ReturnValue(tuple):
   """
   This class is created to contain the return values generated by
   shell.py's run function on the host side.  Inheriting from tuple
   allows us to both:
   Use as a tuple:  The values stored in here can be upacked
   Use as a dict :  The instantiated object will be updated with the
      return dict from shell.py's run function.
   """


class RemoteInstallerOps(Installer):
    """ Implementation of the Installer methods for remote access.

    """

    def __init__(self, installer):
       self._installer = installer
       # This side does not need anything elsecontains no local storage as the main installer
       #  is doing the bulk of the work.


    # All incoming requests will pass through MessageIn.  We need to
    #  define no-ops for these methods in case the component does not
    #  override them.
    #
    # Returning None as ret = method(*args, **kwargs) assigns the
    #  method itself to ret if I only pass.

    def PreTransactionInstall(self, old, new, upgrade):
        return None

    def PreTransactionUninstall(self, old, new, upgrade):
        return None

    def PostTransactionInstall(self, old, new, upgrade):
        return None

    def InitializeQuestions(self, old, new, upgrade):
        return None

    def InitializeInstall(self, old, new, upgrade):
        return None

    def InitializeUninstall(self, old, new, upgrade):
        return None

    def PreInstall(self, old, new, upgrade):
        return None

    def PostInstall(self, old, new, upgrade):
        return None

    def PreUninstall(self, old, new, upgrade):
        return None

    def PostUninstall(self, old, new, upgrade):
        return None

    # All outgoing requests from the component that need to
    #  be passed back to the main VMIS so it can set the correct
    #  variables or look up configuration information in the DB.
    def whoami(self):
        """ Return the name of the calling method """
        return inspect.stack()[1][3]

    def GetConfig(self, key, default=None, component=None):
        """ Get the value for a configuration key """
        # We only need to pass the name, and can't pass component
        # objects over the wire.
        if component:
           component = str(component)
        return self.MessageOut(self.whoami(), str(key), default=default,
                               component=component)

    def SetConfig(self, key, val):
        """ Set the value for a configuration key """
        return self.MessageOut(self.whoami(), str(key), str(val))

    def DelConfig(self, key):
        """ Delete a configuration key """
        return self.MessageOut(self.whoami(), str(key))

    def GetManifestValue(self, key, default=None):
       """ Pass Manifest info to component """
       return self.MessageOut(self.whoami(), str(key), default)

    def GetFileValue(self, key):
        """
        Get the value of one of our special files.
        ie: BINDIR, LIBDIR
        """
        val = path.path(self.MessageOut(self.whoami(), key))
        return val

    def RunCommand(self, *args, **kwargs):
        """ Run an arbitrary command on the local side """
        # Make sure args and kwargs are all strings.  We don't
        # want to pass unexpanded ComponentDestinations over.

        # Grab the returned dict
        ret = self.MessageOut(self.whoami(), *args, **kwargs)

        # Create an object of type ReturnValue.  We want to be
        # able to unpack values at once.  ex:
        #    ret, in, err = RunCommand(.....)
        # ReturnValue inherits from tuple, so when we construct it, it needs
        # to take a tuple as its parameter.
        retval = ReturnValue((ret['retCode'], ret['stdout'], ret['stderr']))

        # Also allow values to be accessed without indexing by updating
        # the object's dict. ex:
        #    retval.retCode, retval.stdout, retval.stderr
        retval.__dict__.update(ret)

        return retval

    def Log(self, logType, *args, **kwargs):
        """ Log a message """
        return self.MessageOut(self.whoami(), logType, *args, **kwargs)

    def SetBannerImage(self, banner):
        """ Set banner image for use during installation """
        return self.MessageOut(self.whoami(), banner)

    def SetIconImages(self, filePaths):
        """ Sets the icons to be used for the window """
        return self.MessageOut(self.whoami(), filePaths)

    def SetHeaderImage(self, imagePath):
        """ Sets the header image """
        return self.MessageOut(self.whoami(), imagePath)

    def GetFileText(self, filePath):
        """ Extracts a file from the component and returns the contents """
        return self.MessageOut(self.whoami(), filePath)

    def UserMessage(self, text, useWrapper=True):
        """ The GUI displays a message to the user """
        return self.MessageOut(self.whoami(), text, useWrapper=useWrapper)

    def AddQuestion(self, questType, *args, **kwargs):
        """ Add a question """
        # Check kwargs and ensure that all PathTemplate variables are
        # converted to strings.
        return self.MessageOut(self.whoami(), questType, *args, **kwargs)

    def AddTarget(self, targetType, src, dest):
        """ Add a file target """
        # Force expansion of src and dest before passing them over
        # to the installer's AddTarget.  Otherwise text like BINDIR
        # won't get expanded correctly
        if isinstance(src, files.Destination):
           fsrc = files.Destination(str(src), src.perm, src.fileType)
        else:
           fsrc = files.Destination(str(src))

        if isinstance(dest, files.Destination):
           fdest = files.Destination(str(dest), dest.perm, dest.fileType)
        else:
           fdest = files.Destination(str(dest))

        return self.MessageOut(self.whoami(), targetType,
                               fsrc, fdest)

    def SetPermission(self, src, perm):
        """ Add a permission to a file """
        if isinstance(src, files.Destination):
           return self.MessageOut(self.whoami(),
                                  files.Destination(src, perm=src.perm,
                                                    fileType=src.fileType),
                                  perm)
        else:
           return self.MessageOut(self.whoami(), files.Destination(src), perm)

    def SetFileType(self, src, fileType):
        """ Set a file type on a file """
        return self.MessageOut(self.whoami(), files.Destination(src), fileType)

    def LoadInclude(self, filename):
       """ Loads and returns a module """
       fd, pathname, desc = imp.find_module(filename,
                                            [self._installer.loadPath + '/include'])
       module = None
       if not fd:
          return None
       try:
          module = imp.load_module(filename, fd, pathname, desc)
       finally:
          fd.close()

       # Update the module with our environment
       for k in dir(self._installer.mod):
          # Ignore __ prefixed symbols
          if (str(k))[0:2] == "__":
             continue
          module.__dict__[k] = self._installer.mod.__dict__[k]
       # And a reference to this object
       module.__dict__['inst'] = self

       return module

    def CompilePythonFile(self, filePath):
       """
       Byte-compiles a Python file for the current component.

       This should only be used in PostInstall for files that are
       dynamically generated and cannot be installed at install time.

       It's advisable to register this file with Register File if it
       needs to be removed by the installer during uninstall.
       """
       compiled = path.path(_compilePython(str(filePath)))
       return compiled

    def RegisterFile(self, filename, mtime=None, fileType='File'):
       """
       Register a file with the installer.
       """
       return self.MessageOut('RegisterFile',
                              files.Destination(filename),
                              mtime=mtime, fileType=fileType)

    def RegisterDirectory(self, dirname, mtime=None, fileType='File'):
       """
       Recursively register all files in a directory with the installer.
       """
       return self.MessageOut('RegisterDirectory',
                              files.Destination(dirname),
                              mtime=mtime, fileType=fileType)

    def RegisterService(self, name, src, start, stop):
       """
       Register a service to be installed.  Caller is still responsible
       for start/stopping services.  This *must* be called in PreInstall or
       earlier in order to register the symlinks with the installer.  If it's
       done later, they won't be laid down.

       @param name: base init script name
       @param src: component source path
       @param start: start priority
       @param stop: stop priority
       """
       return self.MessageOut(self.whoami(), name,
                              files.Destination(src), start, stop)

    # Local setters
    def SetComponent(self, component):
       """ Set the component """
       self._component = component;

    def SetUID(self, UID):
       """ Set the UID """
       self._UID = UID

    # XXX: The code from here down is duplicated for remoteinstallerops.py
    # and remoteinstaller.py at the moment, but with small differences.
    # Find a good way to consolidate them.
    def _rebuildArgs(self, args, kwargs):
       """
       Rebuild args that have been deconstructed into tuples.
       The tuple structure is:

       position   value
       0:         type: string
       1*:         args*: data packed into the object

       It is assumed that types can reconstruct themselves by passing
       args[1:] to __init__
       """
       for typ in [files.Destination, path.path]:
          nargs = ()
          for a in args:
              # If the argument is a tuple and the first item matches the type
              if type(a) is tuple and a[0] == str(typ):
                  dest = typ(*a[1:])
                  nargs = nargs + (dest,)
              else:
                  nargs = nargs + (a,)
          args = nargs

          # Fiddle kwargs to unpack Destinations
          for k, v in kwargs.iteritems():
              if type(v) is tuple and v[0] == str(typ):
                  kwargs[k] = typ(*v[1:])

       return (args, kwargs)

    def _unpackSingleArg(self, arg):
        """
        Helper function for _unpackArgs.  Unpack a single argument to
        a tuple.
        """
        if type(arg) is files.Destination:
            return (str(files.Destination), arg.rawText, arg.perm, arg.fileType)
        # Treat ComponentDestinations as Destinations when outgoing.
        elif str(type(arg)) == "<class '__main__.ComponentDestination'>":
            return (str(files.Destination), arg.rawText, arg.perm, arg.fileType)
        elif type(arg) is path.path:
            return (str(path.path), str(arg))
        else:
            return arg

    def _unpackArgs(self, args, kwargs):
        """
        Scan args and kwargs for any types that need to be broken down
        into a tuple of simpler types.
        We can't allow complex objects to be transferred, as unpickling
        may not be possible.
        """
        for typ in [files.Destination]:
           nargs = ()
           for a in args:
               nargs = nargs + (self._unpackSingleArg(a),)
           args = nargs

           for k, v in kwargs.iteritems():
               kwargs[k] = self._unpackSingleArg(v)

        return (args, kwargs)

    # Communication methods.  These two methods handle the communication
    #  with the main VMIS, both incoming and outgoing messages and route
    #  the flow of control appropriately.
    def MessageIn(self, uid, str):
        """
        This function receives messages from the main VMIS.  Its function
        is to unpack the message, build a function call, and pack up the
        resulting return value or exception and send it back.

        @param uid: The UID of the receiver XXX: Not used at this moment.
        @param str: The message
        """
        self.Log('debug', 'RemoteInstallerOps received message from uid %d as: %s', uid, str)

        # Wrap everything in a try/catch
        try:
            # Unpickle the incoming message
            strio = StringIO.StringIO(str)
            execmethod = pickle.load(strio)
            methodName = pickle.load(strio)
            args = pickle.load(strio)
            kwargs = pickle.load(strio)
            strio.close()

            # Verify that we have received the correct type of message.
            #  If not, raise an exception.
            if execmethod != 'ExecuteMethod':
               raise MalformedMessage('Message to remote component not properly formatted!')

            (args, kwargs) = self._rebuildArgs(args, kwargs)

            # Look up the method name on the component.
            method = getattr(self._installer, methodName, None)
            if method is None:
               raise MethodNotFound('Method: %s not found on remote component!' % methodName)

            # Call the remote method and catch exceptions
            try:
                ret = method(*args, **kwargs)
            except Exception, e:
                # If there is an exception, we want to pickle it up and return.
                strioOut = StringIO.StringIO()

                # Get information on the exception.
                (excType, excValue, tback) = sys.exc_info()
                strList = traceback.format_exception(excType, excValue, tback)
                excValue = '%s' % excValue # Explicitly make it a string.

                # Log it if it's the first time it's been called.
                if excValue.rfind('VMIS:') == -1:
                    self.Log('error', '\n'.join(strList))
                    # Prepend VMIS: to the error text so we don't log later on
                    excValue = 'VMIS:' + excValue

                # Pickle it into strioOut
                pickle.dump('exception', strioOut)
                pickle.dump('%s' % excType, strioOut)
                pickle.dump('%s' % excValue, strioOut)
                self.VerifyArguments(strList)
                pickle.dump(strList, strioOut)
                strioOut.flush()
                retstr = strioOut.getvalue()
                length = len(retstr)
                retval = vmispy.SetReturnValue(self._UID, retstr, length) # XXX: HACK!
                return

            # Pickle and return the return argument.
            strioOut = StringIO.StringIO()
            pickle.dump('return value', strioOut)
            self.VerifyArguments(ret)
            pickle.dump(ret, strioOut)
            strioOut.flush()
            retstr = strioOut.getvalue() # Get the return pickled string
            length = len(retstr)
            retval = vmispy.SetReturnValue(self._UID, retstr, length) # XXX: HACK!
            strioOut.close() # Close the strioOut object
            return
        except Exception, e:
            self.Log('error', 'RemoteOps exception: %s' % e)
            retval = vmispy.SetReturnValue(self._UID, '-1', 0) # XXX: HACK!
            return


    def MessageOut(self, methodName, *args, **kwargs):
        """
        This method is the gateway for all remote database calls.  It is
        responsible for packing the method call into a message and sending
        it out to the main VMIS, as well as returning the return value from
        the remote call to the calling method.

        @param methodname: The method to call on the class
        @param args: Method arguments
        @param kwargs: Method keyword arguments
        """
        (args, kwargs) = self._unpackArgs(args, kwargs)

        # Pickle the query and args and send them
        # to the other end.
        strio = StringIO.StringIO()
        self.VerifyArguments(methodName)
        self.VerifyArguments(args)
        self.VerifyArguments(kwargs)
        pickle.dump('ExecuteMethod', strio)
        pickle.dump(methodName, strio)
        pickle.dump(args, strio)
        pickle.dump(kwargs, strio)
        strio.flush()

        # XXX: Hardcoded 0 here for the main installer.  Also hardcoded
        #  into the C-code.  I'd rather hand a value into here from C than
        #  have it hard coded in two places.  Works for now, but possible source
        #  of error
        retval = vmispy.RunExternalMethod(0, self._UID, strio.getvalue())

        # If the return value is an empty string, then convert it to None, a type unpickle
        # can handle
        if not retval:
           retval = None

        strio.close() # No more ops on the string

        # Interpret the return value and pass it back to the calling method.
        strin = StringIO.StringIO(retval)
        retType = pickle.load(strin)

        # Check if ret is an exception.  If so, we need to raise it here.
        if retType == 'exception':
            # Check the exception type and raise it.
            excepType = pickle.load(strin)
            excepValue = pickle.load(strin)
            excepStr = pickle.load(strin)

            typ = '%s' % excepType
            # These come in the form: <class '__main__.MyError'>
            # Pare it down to the exception class, and grab the Exception type
            typ = typ.split("'")[1] # Grab text in quotes
            typ = typ.split(".")    # Split by .
            typ = typ[len(typ) - 1] # grab the last class name.  It's the one we want
            execType = None
            # First search our global space for errors.
            try:
               execType = globals()[typ]
            except KeyError:
               pass
            # If the exception doesn't exist in the global dict, check against
            # exceptions.
            if not execType:
               try:
                  import exceptions
                  execType = getattr(exceptions, typ)
               except:
                  pass
            # If the exception has been found, raise it, otherwise just
            # raise a generic exception.
            if execType:
               raise execType(excepValue)
            else:
               raise Exception(excepValue)

        elif retType == 'return value':
            # Grab the return type and pass it back
            ret = pickle.load(strin)
            return ret
        else:
            raise TypeError('Unknown return type: %s' % retType)
